-
Notifications
You must be signed in to change notification settings - Fork 3.4k
Add runtime migration creation and application #37415
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
a15611a to
9a35a9b
Compare
62018f1 to
5c41f2f
Compare
test/EFCore.SqlServer.FunctionalTests/Migrations/RuntimeMigrationSqlServerTest.cs
Outdated
Show resolved
Hide resolved
src/EFCore.Design/Design/DesignTimeServiceCollectionExtensions.cs
Outdated
Show resolved
Hide resolved
src/EFCore.Design/Migrations/Design/IDynamicMigrationsAssembly.cs
Outdated
Show resolved
Hide resolved
src/EFCore.Design/Migrations/Design/IDynamicMigrationsAssembly.cs
Outdated
Show resolved
Hide resolved
src/EFCore.Relational/Extensions/RelationalDatabaseFacadeExtensions.cs
Outdated
Show resolved
Hide resolved
This comment was marked as outdated.
This comment was marked as outdated.
test/EFCore.Design.Tests/Design/Internal/MigrationsOperationsTest.cs
Outdated
Show resolved
Hide resolved
- Implement IAsyncLifetime and call Fixture.ReseedAsync() in InitializeAsync instead of manually calling CleanDatabase in each test - Use context.Database.OpenConnection/CloseConnection instead of direct connection.Open/Close calls - Move database cleanup logic to fixture's CleanAsync override - Add GetTableNamesAsync to fixtures for async cleanup
This comment was marked as resolved.
This comment was marked as resolved.
- Simplify CSharpMigrationCompiler.GetMetadataReferences to use cached references plus context assembly, removing explicit Assembly.Load calls - Remove duplicate name validation from AddMigration/AddAndApplyMigration since PrepareForMigration already validates - Remove UsePooling override (controls DbContext pooling, not connection pooling)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR implements runtime migration creation and application to support scenarios like .NET Aspire and containerized applications where recompiling is not possible. It extends the existing dotnet ef database update and Update-Database commands with a new --add option that scaffolds, compiles (using Roslyn), registers, and applies a migration in one atomic operation.
Changes:
- Adds
IMigrationCompilerinterface andCSharpMigrationCompilerimplementation for runtime Roslyn-based compilation of scaffolded migrations - Extends
IMigrationsAssemblywithAddMigrations(Assembly)method to register dynamically compiled migrations - Adds
AddAndApplyMigrationoperation toMigrationsOperationsthat orchestrates the scaffold → compile → register → apply workflow - Updates CLI and PowerShell commands to support
--add,--output-dir,--namespace, and--jsonoptions with appropriate validation
Reviewed changes
Copilot reviewed 26 out of 29 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
src/EFCore.Design/Migrations/Design/IMigrationCompiler.cs |
New internal interface for runtime migration compilation |
src/EFCore.Design/Migrations/Design/CSharpMigrationCompiler.cs |
Roslyn-based implementation with assembly reference caching |
src/EFCore.Relational/Migrations/IMigrationsAssembly.cs |
Adds AddMigrations method to public interface |
src/EFCore.Relational/Migrations/Internal/MigrationsAssembly.cs |
Implements dynamic migration registration with thread-safety concerns |
src/EFCore.Design/Design/Internal/MigrationsOperations.cs |
Core AddAndApplyMigration operation with error handling |
src/EFCore.Design/Design/OperationExecutor.cs |
Operation executor for AddAndApplyMigration command |
src/ef/Commands/DatabaseUpdateCommand*.cs |
CLI command extensions with validation logic |
src/EFCore.Tools/tools/EntityFrameworkCore.psm1 |
PowerShell Update-Database function enhancements |
test/EFCore.Relational.Specification.Tests/RuntimeMigrationTestBase.cs |
Comprehensive test base with 20+ test scenarios |
test/EFCore.*.FunctionalTests/RuntimeMigration*Test.cs |
Provider-specific test implementations |
Resource files (*.resx, *.Designer.cs) |
New localized strings for errors and messages |
Files not reviewed (3)
- src/EFCore.Design/Properties/DesignStrings.Designer.cs: Language not supported
- src/dotnet-ef/Properties/Resources.Designer.cs: Language not supported
- src/ef/Properties/Resources.Designer.cs: Language not supported
src/EFCore.Design/Design/DesignTimeServiceCollectionExtensions.cs
Outdated
Show resolved
Hide resolved
test/EFCore.Relational.Specification.Tests/RuntimeMigrationTestBase.cs
Outdated
Show resolved
Hide resolved
…ions Per reviewer feedback, added a `createTables` parameter (default true) to the test store clean methods. Runtime migration tests use `createTables: false` to get an empty database without tables, allowing migrations to create them. Changes: - Add `bool createTables = true` to RelationalDatabaseCleaner.Clean() - Propagate parameter through SqliteDatabaseCleaner, SqlServerDatabaseCleaner - Add parameter to EnsureClean extension methods - Add parameter to TestStore.CleanAsync and provider implementations - Simplify RuntimeMigrationTestBase to use TestStore.CleanAsync directly - Add UsePooling => false to fixture (pooled contexts retain migration assemblies) - Remove custom CleanAsync overrides from runtime migration fixtures
2e06ca9 to
72a3c0f
Compare
- Add validation that --json requires --add in database update command - Restore original service registration order in DesignTimeServiceCollectionExtensions - Add PowerShell validation for -OutputDir/-Namespace requiring -Add - Add comment documenting empty MigrationFiles JSON behavior
Instead of adding a new custom resource JsonRequiresAdd, reuse the existing MissingConditionalOption resource which provides the same functionality. This avoids issues with T4 template regeneration in CI.
The AddAndApplyMigration tests require a database connection because Migrator.Migrate() calls _connection.Open(). Without a valid connection string, the tests fail in CI. Adding "Data Source=:memory:" provides an in-memory SQLite database that allows the migration operations to complete successfully.
The Migrator.Migrate method opens and closes the connection multiple times during migration (for CreateIfNotExists and MigrateImplementation). With Data Source=:memory:, the SQLite database is destroyed when the connection closes, which causes the migration history table to be lost between steps. Using an externally opened connection ensures EF Core won't close it during migration, keeping the in-memory database alive throughout the operation.
| if (_outputDir!.HasValue()) | ||
| { | ||
| throw new CommandException(Resources.OutputDirRequiresAdd); | ||
| } | ||
|
|
||
| if (_namespace!.HasValue()) | ||
| { | ||
| throw new CommandException(Resources.NamespaceRequiresAdd); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use Resources.MissingConditionalOption for these exceptions as well instead of adding new string resources
| [ConditionalFact] | ||
| public void AddMigration_throws_when_name_is_empty() | ||
| { | ||
| var assembly = MockAssembly.Create(typeof(TestContext)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use AssemblyTestContext in these tests
|
|
||
| if (_json!.HasValue()) | ||
| { | ||
| ReportJson(files); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If files is empty or just contains null values don't report anything.
| throw new OperationException( | ||
| DesignStrings.AddAndApplyMigrationFailed(name, ex.Message), ex); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You don't need to wrap this exception, so remove the try/catch
| var migration = | ||
| string.IsNullOrEmpty(@namespace) | ||
| ? scaffolder.ScaffoldMigration(name, _rootNamespace ?? string.Empty, subNamespace, _language, dryRun: true) | ||
| : scaffolder.ScaffoldMigration(name, null, @namespace, _language, dryRun: true); | ||
|
|
||
| MigrationFiles? files = null; | ||
| try | ||
| { | ||
| files = scaffolder.Save(_projectDir, migration, resolvedOutputDir, dryRun: false); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Move this, as well as scaffolder declaration to a new method that can also be called by AddMigration, make sure to preserve the code comment about #18950.
outputDir and subNamespace calculations can be moved from PrepareForMigration to this new shared method.
| if (_cachedReferences != null) | ||
| { | ||
| return _cachedReferences; | ||
| } | ||
|
|
||
| lock (_referenceLock) | ||
| { | ||
| if (_cachedReferences != null) | ||
| { | ||
| return _cachedReferences; | ||
| } | ||
|
|
||
| var references = new List<MetadataReference>(); | ||
|
|
||
| // Add references from all loaded assemblies (except dynamic/in-memory ones) | ||
| foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies()) | ||
| { | ||
| AddAssemblyReference(references, assembly); | ||
| } | ||
|
|
||
| _cachedReferences = references; | ||
| return _cachedReferences; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use NonCapturingLazyInitializer.EnsureInitialized to avoid using a lock
| /// <returns>The list of metadata references.</returns> | ||
| protected virtual IReadOnlyList<MetadataReference> GetMetadataReferences( | ||
| Type contextType, | ||
| IEnumerable<Assembly>? additionalReferences) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems that additionalReferences is always null, so it can be removed.
| var allReferences = new List<MetadataReference>(baseReferences); | ||
|
|
||
| // Add the context's assembly (in case it wasn't loaded when cache was built) | ||
| AddAssemblyReference(allReferences, contextType.Assembly); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can't see a situation where the context assembly wouldn't be in the cache, so you can remove this call and this whole method can then be inlined
| /// <summary> | ||
| /// Compiles scaffolded migration source code into an in-memory assembly. | ||
| /// </summary> | ||
| /// <param name="scaffoldedMigration">The scaffolded migration containing C# source code.</param> | ||
| /// <param name="contextType">The type of the <see cref="DbContext" /> for which the migration was created.</param> | ||
| /// <param name="references">Additional assembly references to include in compilation, if any.</param> | ||
| /// <returns>An <see cref="Assembly" /> containing the compiled migration and model snapshot.</returns> | ||
| /// <exception cref="InvalidOperationException">Thrown when compilation fails.</exception> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| /// <summary> | |
| /// Compiles scaffolded migration source code into an in-memory assembly. | |
| /// </summary> | |
| /// <param name="scaffoldedMigration">The scaffolded migration containing C# source code.</param> | |
| /// <param name="contextType">The type of the <see cref="DbContext" /> for which the migration was created.</param> | |
| /// <param name="references">Additional assembly references to include in compilation, if any.</param> | |
| /// <returns>An <see cref="Assembly" /> containing the compiled migration and model snapshot.</returns> | |
| /// <exception cref="InvalidOperationException">Thrown when compilation fails.</exception> | |
| /// <summary> | |
| /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to | |
| /// the same compatibility standards as public APIs. It may be changed or removed without notice in | |
| /// any release. You should only use it directly in your code with extreme caution and knowing that | |
| /// doing so can result in application failures when updating to a new Entity Framework Core release. | |
| /// </summary> |
| { | ||
| get | ||
| { | ||
| lock (_lock) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use NonCapturingLazyInitializer.EnsureInitialized here instead of lock
| } | ||
|
|
||
| [ConditionalFact] | ||
| public void CompileMigration_throws_on_very_long_migration_name() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This test is just testing the compiler; remove.
| } | ||
|
|
||
| [ConditionalFact] | ||
| public void CompileMigration_throws_on_invalid_code() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This test is just testing the compiler; remove.
| } | ||
|
|
||
| [ConditionalFact] | ||
| public void Can_compile_migration() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Try to refactor some of these tests to create a MigrationsOperations with a TestOperationReporter and call the extracted methods on MigrationsOperations mentioned in the comments above to reduce code duplication in these tests (the extracted methods would need to be public)
249ae47 to
6b86657
Compare
Summary
Implements #37342: Allow creating and applying migrations at runtime without recompiling.
This adds support for creating and applying migrations at runtime using Roslyn compilation, enabling scenarios like .NET Aspire and containerized applications where recompilation isn't possible.
CLI Usage
The
-o/--output-dir,-n/--namespace, and--jsonoptions require--addto be specified.PowerShell Usage
Architecture
IMigrationCompiler/CSharpMigrationCompilerIMigrationsAssembly.AddMigrations(Assembly)MigrationsOperations.AddAndApplyMigration()Design Decisions
IMigrationsScaffolderfor scaffolding andIMigratorfor applying, adding only the newIMigrationCompilerserviceIMigrationsAssemblyinterface to accept additional assemblies containing runtime-compiled migrationsAddMigration, files are always saved to enable source control and future recompilationIMigrationCompilerandCSharpMigrationCompilerare in the.Internalnamespace as they require design work for public APIMigrationsAssemblyuses locking to protect against race conditions when adding migrations concurrentlyWorkflow
Robustness Features
AddAndApplyMigrationwraps the save-compile-register-apply chain in try-catch, deleting saved files on failure to prevent orphansPrepareForMigrationensures the DbContext is disposed if validation or service building failsMigrationsAssemblyuses locking to protect shared state (migrations dictionary, model snapshot, additional assemblies list)Limitations
[RequiresDynamicCode]Test plan
CSharpMigrationCompilerMigrationsOperations.AddAndApplyMigrationRuntimeMigrationTestBase(SQLite and SQL Server implementations)Fixes #37342